#include "ofBufferObject.h"
#include "ofCubeMap.h"
#include "ofGLProgrammableRenderer.h"
#include "ofLight.h"
#include "ofShader.h"
#include "ofShadow.h"
#ifdef TARGET_ANDROID
    #include "ofxAndroidUtils.h"
#endif
#include <regex>
#include <vector>

using std::endl;
using std::ostringstream;
using std::setw;
using std::string;
using std::stringstream;
using std::unordered_map;
using std::vector;

static const string COLOR_ATTRIBUTE = "color";
static const string POSITION_ATTRIBUTE = "position";
static const string NORMAL_ATTRIBUTE = "normal";
static const string TEXCOORD_ATTRIBUTE = "texcoord";

static unordered_map<GLuint, int> & getShaderIds() {
    static unordered_map<GLuint, int> * ids = new unordered_map<GLuint, int>;
    return *ids;
}

static unordered_map<GLuint, int> & getProgramIds() {
    static unordered_map<GLuint, int> * ids = new unordered_map<GLuint, int>;
    return *ids;
}

//--------------------------------------------------------------
static void retainShader(GLuint id) {
    if (id == 0) return;
    if (getShaderIds().find(id) != getShaderIds().end()) {
        getShaderIds()[id]++;
    } else {
        getShaderIds()[id] = 1;
    }
}

//--------------------------------------------------------------
static void releaseShader(GLuint program, GLuint id) {
    if (getShaderIds().find(id) != getShaderIds().end()) {
        getShaderIds()[id]--;
        if (getShaderIds()[id] == 0) {
            glDetachShader(program, id);
            glDeleteShader(id);
            getShaderIds().erase(id);
        }
    } else {
        ofLogWarning("ofShader") << "releaseShader(): something's wrong here, releasing unknown shader id " << id << " from program " << program;
        glDetachShader(program, id);
        glDeleteShader(id);
    }
}

//--------------------------------------------------------------
static void retainProgram(GLuint id) {
    if (id == 0) return;
    if (getProgramIds().find(id) != getProgramIds().end()) {
        getProgramIds()[id]++;
    } else {
        getProgramIds()[id] = 1;
    }
}

//--------------------------------------------------------------
static void releaseProgram(GLuint id) {
    if (getProgramIds().find(id) != getProgramIds().end()) {
        getProgramIds()[id]--;
        if (getProgramIds()[id] == 0) {
            glDeleteProgram(id);
            getProgramIds().erase(id);
        }
    } else {
        ofLogWarning("ofShader") << "releaseProgram(): something's wrong here, releasing unknown program id " << id;
        glDeleteProgram(id);
    }
}

#if !defined(TARGET_OPENGLES) || defined(TARGET_EMSCRIPTEN)
//--------------------------------------------------------------
ofShader::TransformFeedbackRangeBinding::TransformFeedbackRangeBinding(const ofBufferObject & buffer, GLuint offset, GLuint size)
    : offset(offset)
    , size(size)
    , buffer(buffer) { }

//--------------------------------------------------------------
ofShader::TransformFeedbackBaseBinding::TransformFeedbackBaseBinding(const ofBufferObject & buffer)
    : buffer(buffer) { }
#endif

//--------------------------------------------------------------
ofShader::ofShader()
    : program(0)
    , bLoaded(false) {
}

//--------------------------------------------------------------
ofShader::~ofShader() {
    unload();
}

//--------------------------------------------------------------
ofShader::ofShader(const ofShader & mom)
    : program(mom.program)
    , bLoaded(mom.bLoaded)
    , shaders(mom.shaders)
    , uniformsCache(mom.uniformsCache)
    , attributesBindingsCache(mom.attributesBindingsCache)
#ifndef TARGET_OPENGLES
    , uniformBlocksCache(mom.uniformBlocksCache)
#endif
{
    if (mom.bLoaded) {
        retainProgram(program);
        for (auto it : shaders) {
            auto shader = it.second;
            retainShader(shader.id);
        }
#ifdef TARGET_ANDROID
        ofAddListener(ofxAndroidEvents().unloadGL, this, &ofShader::unloadGL);
#endif
    }
}

//--------------------------------------------------------------
ofShader & ofShader::operator=(const ofShader & mom) {
    if (this == &mom) {
        return *this;
    }
    if (bLoaded) {
        unload();
    }
    program = mom.program;
    bLoaded = mom.bLoaded;
    shaders = mom.shaders;
    attributesBindingsCache = mom.attributesBindingsCache;
    uniformsCache = mom.uniformsCache;
    if (mom.bLoaded) {
        retainProgram(program);
        for (auto it : shaders) {
            auto shader = it.second;
            retainShader(shader.id);
        }
#ifdef TARGET_ANDROID
        ofAddListener(ofxAndroidEvents().unloadGL, this, &ofShader::unloadGL);
#endif
    }
    return *this;
}

ofShader::ofShader(ofShader && mom)
    : program(std::move(mom.program))
    , bLoaded(std::move(mom.bLoaded))
    , shaders(std::move(mom.shaders))
    , uniformsCache(std::move(mom.uniformsCache))
    , attributesBindingsCache(std::move(mom.attributesBindingsCache)) {
    if (mom.bLoaded) {
#ifdef TARGET_ANDROID
        ofAddListener(ofxAndroidEvents().unloadGL, this, &ofShader::unloadGL);
#endif
    }
    mom.bLoaded = false;
}

ofShader & ofShader::operator=(ofShader && mom) {
    if (this == &mom) {
        return *this;
    }
    if (bLoaded) {
        unload();
    }
    program = std::move(mom.program);
    bLoaded = std::move(mom.bLoaded);
    shaders = std::move(mom.shaders);
    attributesBindingsCache = std::move(mom.attributesBindingsCache);
    uniformsCache = std::move(mom.uniformsCache);
    if (mom.bLoaded) {
#ifdef TARGET_ANDROID
        ofAddListener(ofxAndroidEvents().unloadGL, this, &ofShader::unloadGL);
#endif
    }
    mom.bLoaded = false;
    return *this;
}

//--------------------------------------------------------------
bool ofShader::load(const of::filesystem::path & shaderName) {
	auto vertFile = shaderName;
	auto fragFile = shaderName;
	vertFile += ".vert";
	fragFile += ".frag";
//    return load(shaderName + ".vert", shaderName + ".frag");
	return load(vertFile, fragFile);
}

//--------------------------------------------------------------
bool ofShader::load(const of::filesystem::path & vertName, const of::filesystem::path & fragName, const of::filesystem::path & geomName) {
    if (vertName.empty() == false) setupShaderFromFile(GL_VERTEX_SHADER, vertName);
    if (fragName.empty() == false) setupShaderFromFile(GL_FRAGMENT_SHADER, fragName);
#ifndef TARGET_OPENGLES
    if (geomName.empty() == false) setupShaderFromFile(GL_GEOMETRY_SHADER_EXT, geomName);
#endif
    if (ofIsGLProgrammableRenderer()) {
        bindDefaults();
    }
    return linkProgram();
}

#if !defined(TARGET_OPENGLES) && defined(glDispatchCompute)
//--------------------------------------------------------------
bool ofShader::loadCompute(const of::filesystem::path & shaderName) {
    return setupShaderFromFile(GL_COMPUTE_SHADER, shaderName) && linkProgram();
}
#endif

//--------------------------------------------------------------
bool ofShader::setup(const ofShaderSettings & settings) {
    for (auto shader : settings.shaderFiles) {
        auto ty = shader.first;
        auto file = shader.second;
        auto shaderSource = sourceFromFile(ty, file);
        shaderSource.intDefines = settings.intDefines;
        shaderSource.floatDefines = settings.floatDefines;
        if (!setupShaderFromSource(std::move(shaderSource))) {
            return false;
        }
    }

    for (auto shader : settings.shaderSources) {
        auto ty = shader.first;
        auto source = shader.second;
        Source shaderSource { ty, source, settings.sourceDirectoryPath };
        shaderSource.intDefines = settings.intDefines;
        shaderSource.floatDefines = settings.floatDefines;
        if (!setupShaderFromSource(std::move(shaderSource))) {
            return false;
        }
    }

    if (ofIsGLProgrammableRenderer() && settings.bindDefaults) {
        bindDefaults();
    }

    return linkProgram();
}

#if !defined(TARGET_OPENGLES) || defined(TARGET_EMSCRIPTEN)
//--------------------------------------------------------------
bool ofShader::setup(const TransformFeedbackSettings & settings) {
    for (auto shader : settings.shaderFiles) {
        auto ty = shader.first;
        auto file = shader.second;
        auto shaderSource = sourceFromFile(ty, file);
        shaderSource.intDefines = settings.intDefines;
        shaderSource.floatDefines = settings.floatDefines;
        if (!setupShaderFromSource(std::move(shaderSource))) {
            return false;
        }
    }

    for (auto shader : settings.shaderSources) {
        auto ty = shader.first;
        auto source = shader.second;
        Source shaderSource { ty, source, settings.sourceDirectoryPath };
        shaderSource.intDefines = settings.intDefines;
        shaderSource.floatDefines = settings.floatDefines;
        if (!setupShaderFromSource(std::move(shaderSource))) {
            return false;
        }
    }

    if (ofIsGLProgrammableRenderer() && settings.bindDefaults) {
        bindDefaults();
    }

    if (!settings.varyingsToCapture.empty()) {
        std::vector<const char *> varyings(settings.varyingsToCapture.size());
        std::transform(settings.varyingsToCapture.begin(), settings.varyingsToCapture.end(), varyings.begin(), [](const std::string & str) {
            return str.c_str();
        });
        glTransformFeedbackVaryings(getProgram(), varyings.size(), varyings.data(), settings.bufferMode);
    }
    return linkProgram();
}
#endif

//--------------------------------------------------------------
bool ofShader::setupShaderFromFile(GLenum type, const of::filesystem::path & filename) {
    ofBuffer buffer = ofBufferFromFile(filename);
    // we need to make absolutely sure to have an absolute path here, so that any #includes
    // within the shader files have a root directory to traverse from.
    auto absoluteFilePath = ofFilePath::getAbsolutePath(filename, true);
    auto sourceDirectoryPath = ofFilePath::getEnclosingDirectory(absoluteFilePath, false);
    if (buffer.size()) {
        return setupShaderFromSource(type, buffer.getText(), sourceDirectoryPath);
    } else {
        ofLogError("ofShader") << "setupShaderFromFile(): couldn't load " << nameForType(type) << " shader "
                               << " from \"" << absoluteFilePath << "\"";
        return false;
    }
}

//--------------------------------------------------------------
ofShader::Source ofShader::sourceFromFile(GLenum type, const of::filesystem::path & filename) {
    ofBuffer buffer = ofBufferFromFile(filename);
    // we need to make absolutely sure to have an absolute path here, so that any #includes
    // within the shader files have a root directory to traverse from.
    auto absoluteFilePath = ofFilePath::getAbsolutePath(filename, true);
    auto sourceDirectoryPath = ofFilePath::getEnclosingDirectory(absoluteFilePath, false);
    if (buffer.size()) {
        // return Source{type, buffer.getText(), sourceDirectoryPath.string() };
        return Source { type, buffer.getText(), sourceDirectoryPath };
    } else {
        ofLogError("ofShader") << "setupShaderFromFile(): couldn't load " << nameForType(type) << " shader "
                               << " from \"" << absoluteFilePath << "\"";
        return Source {};
    }
}

//--------------------------------------------------------------
// FIXME: change to of::filesystem
bool ofShader::setupShaderFromSource(GLenum type, string source, of::filesystem::path sourceDirectoryPath) {
    return setupShaderFromSource({ type, source, sourceDirectoryPath });
}

//--------------------------------------------------------------
bool ofShader::setupShaderFromSource(ofShader::Source && source) {
    unload();

    // create program if it doesn't exist already
    checkAndCreateProgram();
    GLuint clearErrors = glGetError(); //needed for some users to clear gl errors
    if (clearErrors != GL_NO_ERROR) {
        ofLogVerbose("ofShader") << "setupShaderFromSource(): OpenGL error after checkAndCreateProgram() (probably harmless): error " << clearErrors;
    }

    // create shader
    GLuint shaderId = glCreateShader(source.type);
    if (shaderId == 0) {
        ofLogError("ofShader") << "setupShaderFromSource(): failed creating " << nameForType(source.type) << " shader";
        return false;
    } else {
        // if the shader object has been allocated successfully on the GPU
        // we must retain it so that it can be de-allocated again, once
        // this ofShader object has been discarded, or re-allocated.
        // we need to do this at this point in the code path, since early
        // return statements might prevent us from retaining later.
        retainShader(shaderId);
    }

    // look for OF_GLSL_SHADER_HEADER header placeholder
    // this will swap the glsl version based on the OpenGL version set in main.cpp
    // note this won't always work, but is handy for switching between compatible versions of GLSL based on the system
    if (ofIsStringInString(source.source, "OF_GLSL_SHADER_HEADER")) {
        ofStringReplace(source.source, "OF_GLSL_SHADER_HEADER", ofGLSLGetDefaultHeader());
    }

    // parse for includes
    source.expandedSource = parseForIncludes(source.source, source.directoryPath);

    // parse and set defines
    for (auto & define : source.intDefines) {
        const auto & name = define.first;
        const auto & value = define.second;
        std::regex re_define("#define[ \t]+" + name + "[ \t]+(([1-9][0-9]*)|(0([xX][0-9a-fA-F]+)?))");
        source.expandedSource = std::regex_replace(source.expandedSource, re_define, "#define " + name + " " + std::to_string(value));
    }

    for (auto & define : source.floatDefines) {
        const auto & name = define.first;
        const auto & value = define.second;
        std::regex re_define("#define[ \t]+" + name + "[ \t]+[0-9]*(\\.[0-9]*f?)?");
        source.expandedSource = std::regex_replace(source.expandedSource, re_define, "#define " + name + " " + std::to_string(value));
    }

    // store source code (that's the expanded source with all includes copied in)
    // we need to store this here, and before shader compilation,
    // so that any shader compilation errors can be
    // traced down to the correct shader source code line.
    shaders[source.type] = { shaderId, std::move(source) };
    auto & shader = shaders[source.type];

    // compile shader
    const char * sptr = shader.source.expandedSource.c_str();
    int ssize = shader.source.expandedSource.size();
    glShaderSource(shaderId, 1, &sptr, &ssize);
    glCompileShader(shaderId);

    // check compile status
    GLint status = GL_FALSE;
    glGetShaderiv(shaderId, GL_COMPILE_STATUS, &status);
    GLuint err = glGetError();
    if (err != GL_NO_ERROR) {
        ofLogError("ofShader") << "setupShaderFromSource(): OpenGL generated error " << err << " trying to get the compile status for a " << nameForType(shader.source.type) << " shader, does your video card support this?";
        return false;
    }

    if (status == GL_TRUE) {
        ofLogVerbose("ofShader") << "setupShaderFromSource(): " << nameForType(shader.source.type) + " shader compiled";
#ifdef TARGET_EMSCRIPTEN
        checkShaderInfoLog(shaderId, source.type, OF_LOG_VERBOSE);
#else
        checkShaderInfoLog(shaderId, source.type, OF_LOG_WARNING);
#endif
    } else if (status == GL_FALSE) {
        ofLogError("ofShader") << "setupShaderFromSource(): " << nameForType(shader.source.type) + " shader failed to compile";
        checkShaderInfoLog(shaderId, source.type, OF_LOG_ERROR);
        return false;
    }
    return true;
}

/*
 * Parse for GLSL includes based on
 * https://www.opengl.org/discussion_boards/showthread.php/169209-include-in-glsl?p=1192415&viewfull=1#post1192415
 */

//--------------------------------------------------------------
string ofShader::parseForIncludes(const string & source, const of::filesystem::path & sourceDirectoryPath) {
    vector<of::filesystem::path> included;
    return parseForIncludes(source, included, 0, sourceDirectoryPath);
}

//--------------------------------------------------------------
// FIXME: update to use fs::path in vector and source
string ofShader::parseForIncludes(const string & source, vector<of::filesystem::path> & included, int level, const of::filesystem::path & sourceDirectoryPath) {

    if (level > 32) {
        ofLogError("ofShader") << "glsl header inclusion depth limit reached, might be caused by cyclic header inclusion";
        return "";
    }

    stringstream output;
    stringstream input;
    input << source;

    auto match_pragma_include = [](const std::string & s_, std::string & filename_) -> bool {
        filename_ = "";
        std::istringstream s(s_);
        s >> std::ws; // eat up any leading whitespace.

        if (s.peek() != '#') return false;
        // -----| invariant: found '#'
        s.seekg(1, std::ios::cur); // move forward one character

        std::string p, i, f;

        // while skipping whitespace, read in tokens for: pragma, include, and filename
        s >> std::skipws >> p >> i >> f;

        if (p.empty())
            return false;
        else if (p == "include") {
            f = i;
            i = p;
        } else if (p != "pragma")
            return false;

        if (i.empty() || (f.size() < 2))
            return false;

        if (i != "include") return false;

        // first and last character of filename token must match and be either
        // '<' and '>', or '"

        if (f[0] == '<' && f[f.size() - 1] != '>') return false; //< mismatching brackets

        if ((f[0] == '"' || f[0] == '\'') && (f[0] != f[f.size() - 1])) return false; // mismatching quotes

        // invariant: filename properly quoted.

        filename_ = f.substr(1, f.size() - 2);

        return true;
    };

    // once std::regex is available across the board, use this regex in favour of the above lambda:
    // std::regex re("^\\s*#\\s*pragma\\s+include\\s+[\"<](.*)[\">].*");

    string line;
    while (std::getline(input, line)) {

        string include;

        if (!match_pragma_include(line, include)) {
            output << line << endl;
            continue;
        };

        // --------| invariant: '#pragma include' has been requested

		of::filesystem::path includeFS { sourceDirectoryPath / include };
		of::filesystem::path includeFSAbsolute { of::filesystem::absolute(includeFS) };

        if (std::find(included.begin(), included.end(), includeFSAbsolute) != included.end()) {
            ofLogVerbose("ofShader") << include << " already included";
            continue;
        }

        // we store the absolute paths so as have (more) unique file identifiers.
        included.push_back(includeFSAbsolute);

        ofBuffer buffer { ofBufferFromFile(includeFS) };
        if (!buffer.size()) {
            ofLogError("ofShader") << "Could not open glsl include file " << includeFS;
            continue;
        }

		auto currentDir = includeFS.parent_path();
        output << parseForIncludes(buffer.getText(), included, level + 1, currentDir) << endl;
    }

    return output.str();
}

//--------------------------------------------------------------
string ofShader::getShaderSource(GLenum type) const {
    auto source = shaders.find(type);
    if (source != shaders.end()) {
        return source->second.source.expandedSource;
    } else {
        ofLogError("ofShader") << "No shader source for shader of type: " << nameForType(type);
        return "";
    }
}

//--------------------------------------------------------------
void ofShader::setGeometryInputType(GLenum type) {
#ifndef TARGET_OPENGLES
    checkAndCreateProgram();
    glProgramParameteri(program, GL_GEOMETRY_INPUT_TYPE_EXT, type);
#endif
}

//--------------------------------------------------------------
void ofShader::setGeometryOutputType(GLenum type) {
#ifndef TARGET_OPENGLES
    checkAndCreateProgram();
    glProgramParameteri(program, GL_GEOMETRY_OUTPUT_TYPE_EXT, type);
#endif
}

//--------------------------------------------------------------
void ofShader::setGeometryOutputCount(int count) {
#ifndef TARGET_OPENGLES
    checkAndCreateProgram();
    glProgramParameteri(program, GL_GEOMETRY_VERTICES_OUT_EXT, count);
#endif
}

//--------------------------------------------------------------
int ofShader::getGeometryMaxOutputCount() const {
#ifndef TARGET_OPENGLES
    int temp;
    glGetIntegerv(GL_MAX_GEOMETRY_OUTPUT_VERTICES_EXT, &temp);
    return temp;
#else
    return 0;
#endif
}

//--------------------------------------------------------------
bool ofShader::checkProgramLinkStatus() {
    GLint status;
    glGetProgramiv(program, GL_LINK_STATUS, &status);
    GLuint err = glGetError();
    if (err != GL_NO_ERROR) {
        ofLogError("ofShader") << "checkProgramLinkStatus(): OpenGL generated error " << err << " trying to get the program link status, does your video card support shader programs?";
        return false;
    }
    if (status == GL_TRUE)
        ofLogVerbose("ofShader") << "checkProgramLinkStatus(): program " << program << " linked";
    else if (status == GL_FALSE) {
        ofLogError("ofShader") << "checkProgramLinkStatus(): program failed to link";
        checkProgramInfoLog();
        return false;
    }
    return true;
}

//--------------------------------------------------------------
void ofShader::checkShaderInfoLog(GLuint shader, GLenum type, ofLogLevel logLevel) {
    GLsizei infoLength;
    glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &infoLength);
    if (infoLength > 1) {
        ofBuffer infoBuffer;
        infoBuffer.allocate(infoLength);
        glGetShaderInfoLog(shader, infoLength, &infoLength, infoBuffer.getData());
        ofLog(logLevel, "ofShader: %s shader reports:\n%s", nameForType(type).c_str(), infoBuffer.getText().c_str());
#if (!defined(TARGET_LINUX) || defined(GCC_HAS_REGEX))
        if (shaders.find(type) != shaders.end()) {
            // The following regexp should match shader compiler error messages by Nvidia and ATI.
            // Unfortunately, each vendor's driver formats error messages slightly different.
            std::regex nvidia_ati("^.*[(:]{1}(\\d+)[:)]{1}.*");
            std::regex intel("^[0-9]+:([0-9]+)\\([0-9]+\\):.*$");
            std::smatch matches;
            string infoString = ofTrim(infoBuffer);
            if (std::regex_search(infoString, matches, intel) || std::regex_search(infoString, matches, nvidia_ati)) {
                ofBuffer buf;
                buf.set(shaders[type].source.expandedSource);
                ofBuffer::Line line = buf.getLines().begin();
                int offendingLineNumber = ofToInt(matches[1]);
                ostringstream msg;
                msg << "ofShader: " + nameForType(type) + ", offending line " << offendingLineNumber << ":" << endl;
                for (int i = 0; line != buf.getLines().end(); line++, i++) {
                    string s = *line;
                    if (i >= offendingLineNumber - 3 && i < offendingLineNumber + 2) {
                        msg << "\t" << setw(5) << (i + 1) << "\t" << s << endl;
                    }
                }
                ofLog(logLevel) << msg.str();
            } else {
                ofLog(logLevel) << shaders[type].source.expandedSource;
            }
        }
#endif
    }
}

//--------------------------------------------------------------
void ofShader::checkProgramInfoLog() {
    GLsizei infoLength;
    glGetProgramiv(program, GL_INFO_LOG_LENGTH, &infoLength);
    if (infoLength > 1) {
        ofBuffer infoBuffer;
        infoBuffer.allocate(infoLength);
        glGetProgramInfoLog(program, infoLength, &infoLength, infoBuffer.getData());
        // TODO: it appears that Raspberry Pi only reports shader errors whilst linking,
        // but then it becomes hard to figure out whether the fragment or the
        // vertex shader caused the error.
        // We need to find a robust way of extracing this information from
        // the log, and unfortunately can't use regex whilst gcc on RPi is assumed to
        // be < 4.9, which is the first version fully supporting this c++11 feature.
        string msg = "ofShader: program reports:\n";
        ofLogError("ofShader") << msg + infoBuffer.getText();
#ifdef TARGET_RAPSBERRY_PI
        for (auto it : shaders) {
            ofLogNotice("ofShader") << it.second.expandedSource;
        }
#endif
    }
}

//--------------------------------------------------------------
void ofShader::checkAndCreateProgram() {
#ifndef TARGET_OPENGLES
    if (GL_ARB_shader_objects) {
#else
    if (ofIsGLProgrammableRenderer()) {
#endif
        if (program == 0) {
            ofLogVerbose("ofShader") << "checkAndCreateProgram(): creating GLSL program";
            program = glCreateProgram();
            retainProgram(program);
        }
    } else {
        ofLogError("ofShader") << "sorry, it looks like you can't run 'ARB_shader_objects'";
        ofLogError("ofShader") << "please check the capabilites of your graphics card: http://www.ozone3d.net/gpu_caps_viewer";
    }
}

//--------------------------------------------------------------
bool ofShader::linkProgram() {
    if (shaders.empty()) {
        ofLogError("ofShader") << "linkProgram(): trying to link GLSL program, but no shaders created yet";
    } else {
        checkAndCreateProgram();

        for (auto it : shaders) {
            auto shader = it.second;
            if (shader.id > 0) {
                ofLogVerbose("ofShader") << "linkProgram(): attaching " << nameForType(it.first) << " shader to program " << program;
                glAttachShader(program, shader.id);
            }
        }

        glLinkProgram(program);

        checkProgramLinkStatus();

        // Pre-cache all active uniforms
        GLint numUniforms = 0;
        glGetProgramiv(program, GL_ACTIVE_UNIFORMS, &numUniforms);

        GLint uniformMaxLength = 0;
        glGetProgramiv(program, GL_ACTIVE_UNIFORM_MAX_LENGTH, &uniformMaxLength);

        GLint count = -1;
        GLenum type = 0;
        GLsizei length;
        GLint location;
        vector<GLchar> uniformName(uniformMaxLength);
        for (GLint i = 0; i < numUniforms; i++) {
            glGetActiveUniform(program, i, uniformMaxLength, &length, &count, &type, uniformName.data());
            string name(uniformName.begin(), uniformName.begin() + length);
            // some drivers return uniform_name[0] for array uniforms
            // instead of the real uniform name
            location = glGetUniformLocation(program, name.c_str());
            if (location == -1) continue; // ignore uniform blocks

            uniformsCache[name] = location;
            auto arrayPos = name.find('[');
            if (arrayPos != std::string::npos) {
                name = name.substr(0, arrayPos);
                uniformsCache[name] = location;
            }
        }

#ifndef TARGET_OPENGLES
    #ifdef GLEW_ARB_uniform_buffer_object
        if (GLEW_ARB_uniform_buffer_object) {
            // Pre-cache all active uniforms blocks
            GLint numUniformBlocks = 0;
            glGetProgramiv(program, GL_ACTIVE_UNIFORM_BLOCKS, &numUniformBlocks);

            count = -1;
            type = 0;
            vector<GLchar> uniformBlockName(uniformMaxLength);
            for (GLint i = 0; i < numUniformBlocks; i++) {
                glGetActiveUniformBlockName(program, i, uniformMaxLength, &length, uniformBlockName.data());
                string name(uniformBlockName.begin(), uniformBlockName.begin() + length);
                uniformBlocksCache[name] = glGetUniformBlockIndex(program, name.c_str());
            }
        }
    #endif
#endif

#ifdef TARGET_ANDROID
        ofAddListener(ofxAndroidEvents().unloadGL, this, &ofShader::unloadGL);
#endif

        // bLoaded means we have loaded shaders onto the graphics card;
        // it doesn't necessarily mean that these shaders have compiled and linked successfully.
        bLoaded = true;
    }
    return bLoaded;
}

//--------------------------------------------------------------
#ifdef TARGET_ANDROID
void ofShader::unloadGL() {
    for (auto it : shaders) {
        auto shader = it.second;
        if (shader.id) {
            releaseShader(program, shader.id);
        }
    }

    if (program) {
        releaseProgram(program);
        program = 0;
    }
    bLoaded = false;
    ofAddListener(ofxAndroidEvents().reloadGL, this, &ofShader::reloadGL);
}

void ofShader::reloadGL() {
    auto source = shaders;
    auto bindings = attributesBindingsCache;
    shaders.clear();
    uniformsCache.clear();
    #ifndef TARGET_OPENGLES
        #ifdef GLEW_ARB_uniform_buffer_object // Core in OpenGL 3.1
    uniformBlocksCache.clear();
        #endif
    #endif
    attributesBindingsCache.clear();
    for (auto & shader : source) {
        auto source = shader.second.source;
        setupShaderFromSource(std::move(shader.second.source));
    }
    for (auto binding : bindings) {
        bindAttribute(binding.second, binding.first);
    }
    linkProgram();
}
#endif

//--------------------------------------------------------------
void ofShader::bindAttribute(GLuint location, const string & name) const {
    attributesBindingsCache[name] = location;
    glBindAttribLocation(program, location, name.c_str());
}

//--------------------------------------------------------------
bool ofShader::bindDefaults() {
    if (shaders.empty()) {
        ofLogError("ofShader") << "bindDefaults(): trying to link GLSL program, but no shaders created yet";
        return false;
    } else {
        bindAttribute(ofShader::POSITION_ATTRIBUTE, ::POSITION_ATTRIBUTE);
        bindAttribute(ofShader::COLOR_ATTRIBUTE, ::COLOR_ATTRIBUTE);
        bindAttribute(ofShader::NORMAL_ATTRIBUTE, ::NORMAL_ATTRIBUTE);
        bindAttribute(ofShader::TEXCOORD_ATTRIBUTE, ::TEXCOORD_ATTRIBUTE);
        return true;
    }
}

//--------------------------------------------------------------
void ofShader::unload() {
    if (bLoaded) {
        for (auto it : shaders) {
            auto shader = it.second;
            if (shader.id) {
                ofLogVerbose("ofShader") << "unload(): detaching and deleting " << nameForType(shader.source.type) << " shader from program " << program;
                releaseShader(program, shader.id);
            }
        }

        if (program) {
            releaseProgram(program);
            program = 0;
        }

        shaders.clear();
        uniformsCache.clear();
#ifndef TARGET_OPENGLES
    #ifdef GLEW_ARB_uniform_buffer_object // Core in OpenGL 3.1
        uniformBlocksCache.clear();
    #endif
#endif
        attributesBindingsCache.clear();
#ifdef TARGET_ANDROID
        ofRemoveListener(ofxAndroidEvents().reloadGL, this, &ofShader::reloadGL);
        ofRemoveListener(ofxAndroidEvents().unloadGL, this, &ofShader::unloadGL);
#endif
    }
    bLoaded = false;
}

//--------------------------------------------------------------
bool ofShader::isLoaded() const {
    return bLoaded;
}

//--------------------------------------------------------------
void ofShader::begin() const {
    ofGetGLRenderer()->bind(*this);
}

//--------------------------------------------------------------
void ofShader::end() const {
    ofGetGLRenderer()->unbind(*this);
}

#if !defined(TARGET_OPENGLES) || defined(TARGET_EMSCRIPTEN)
//--------------------------------------------------------------
void ofShader::beginTransformFeedback(GLenum mode) const {
    begin();
    glEnable(GL_RASTERIZER_DISCARD);
    glBeginTransformFeedback(mode);
}

//--------------------------------------------------------------
void ofShader::beginTransformFeedback(GLenum mode, const TransformFeedbackRangeBinding & binding) const {
    binding.buffer.bindRange(GL_TRANSFORM_FEEDBACK_BUFFER, binding.index, binding.offset, binding.size);
    beginTransformFeedback(mode);
}

//--------------------------------------------------------------
void ofShader::beginTransformFeedback(GLenum mode, const std::vector<TransformFeedbackRangeBinding> & bindings) const {
    for (auto & binding : bindings) {
        binding.buffer.bindRange(GL_TRANSFORM_FEEDBACK_BUFFER, binding.index, binding.offset, binding.size);
    }
    beginTransformFeedback(mode);
}

//--------------------------------------------------------------
void ofShader::beginTransformFeedback(GLenum mode, const TransformFeedbackBaseBinding & binding) const {
    binding.buffer.bindBase(GL_TRANSFORM_FEEDBACK_BUFFER, binding.index);
    beginTransformFeedback(mode);
}

//--------------------------------------------------------------
void ofShader::beginTransformFeedback(GLenum mode, const std::vector<TransformFeedbackBaseBinding> & bindings) const {
    for (auto & binding : bindings) {
        binding.buffer.bindBase(GL_TRANSFORM_FEEDBACK_BUFFER, binding.index);
    }
    beginTransformFeedback(mode);
}

//--------------------------------------------------------------
void ofShader::endTransformFeedback() const {
    glEndTransformFeedback();
    glDisable(GL_RASTERIZER_DISCARD);
    end();
}

//--------------------------------------------------------------
void ofShader::endTransformFeedback(const TransformFeedbackRangeBinding & binding) const {
    binding.buffer.unbindRange(GL_TRANSFORM_FEEDBACK_BUFFER, binding.index);
    endTransformFeedback();
}

//--------------------------------------------------------------
void ofShader::endTransformFeedback(const std::vector<TransformFeedbackRangeBinding> & bindings) const {
    for (auto & binding : bindings) {
        binding.buffer.unbindRange(GL_TRANSFORM_FEEDBACK_BUFFER, binding.index);
    }
    endTransformFeedback();
}

//--------------------------------------------------------------
void ofShader::endTransformFeedback(const TransformFeedbackBaseBinding & binding) const {
    binding.buffer.unbindBase(GL_TRANSFORM_FEEDBACK_BUFFER, binding.index);
    endTransformFeedback();
}

//--------------------------------------------------------------
void ofShader::endTransformFeedback(const std::vector<TransformFeedbackBaseBinding> & bindings) const {
    for (auto & binding : bindings) {
        binding.buffer.unbindBase(GL_TRANSFORM_FEEDBACK_BUFFER, binding.index);
    }
    endTransformFeedback();
}
#endif

#if !defined(TARGET_OPENGLES) && defined(glDispatchCompute)
//--------------------------------------------------------------
void ofShader::dispatchCompute(GLuint x, GLuint y, GLuint z) const {
    glDispatchCompute(x, y, z);
}
#endif

//--------------------------------------------------------------
void ofShader::setUniformTexture(const string & name, const ofBaseHasTexture & img, int textureLocation) const {
    setUniformTexture(name, img.getTexture(), textureLocation);
}

//--------------------------------------------------------------
void ofShader::setUniformTexture(const string & name, int textureTarget, GLint textureID, int textureLocation) const {
    if (bLoaded) {
        glActiveTexture(GL_TEXTURE0 + textureLocation);
        if (!ofIsGLProgrammableRenderer()) {
            glEnable(textureTarget);
            glBindTexture(textureTarget, textureID);
            glDisable(textureTarget);
        } else {
            glBindTexture(textureTarget, textureID);
        }
        setUniform1i(name, textureLocation);
        glActiveTexture(GL_TEXTURE0);
    }
}


//--------------------------------------------------------------
void ofShader::setUniformTexture(const string & name, const ofTexture & tex, int textureLocation) const {
	if (bLoaded) {
		ofTextureData texData = tex.getTextureData();
		setUniformTexture( name, texData, textureLocation);
	}
}

//--------------------------------------------------------------
void ofShader::setUniformTexture(const string & name, const ofTextureData & texData, int textureLocation) const{
    if (bLoaded) {
        //ofTextureData texData = tex.getTextureData();
        glActiveTexture(GL_TEXTURE0 + textureLocation);
        if (!ofIsGLProgrammableRenderer()) {
            glEnable(texData.textureTarget);
            glBindTexture(texData.textureTarget, texData.textureID);
#ifndef TARGET_OPENGLES
            if (texData.bufferId != 0) {
                glTexBuffer(GL_TEXTURE_BUFFER, texData.glInternalFormat, texData.bufferId);
            }
#endif
            glDisable(texData.textureTarget);
        } else {
            glBindTexture(texData.textureTarget, texData.textureID);
#ifndef TARGET_OPENGLES
            if (texData.bufferId != 0) {
                glTexBuffer(GL_TEXTURE_BUFFER, texData.glInternalFormat, texData.bufferId);
            }
#endif
        }
        setUniform1i(name, textureLocation);
        glActiveTexture(GL_TEXTURE0);
    }
}

//--------------------------------------------------------------
void ofShader::setUniform1i(const string & name, int v1) const {
    if (bLoaded) {
        int loc = getUniformLocation(name);
        if (loc != -1) glUniform1i(loc, v1);
    }
}

//--------------------------------------------------------------
void ofShader::setUniform2i(const string & name, int v1, int v2) const {
    if (bLoaded) {
        int loc = getUniformLocation(name);
        if (loc != -1) glUniform2i(loc, v1, v2);
    }
}

//--------------------------------------------------------------
void ofShader::setUniform3i(const string & name, int v1, int v2, int v3) const {
    if (bLoaded) {
        int loc = getUniformLocation(name);
        if (loc != -1) glUniform3i(loc, v1, v2, v3);
    }
}

//--------------------------------------------------------------
void ofShader::setUniform4i(const string & name, int v1, int v2, int v3, int v4) const {
    if (bLoaded) {
        int loc = getUniformLocation(name);
        if (loc != -1) glUniform4i(loc, v1, v2, v3, v4);
    }
}

//--------------------------------------------------------------
void ofShader::setUniform1f(const string & name, float v1) const {
    if (bLoaded) {
        int loc = getUniformLocation(name);
        if (loc != -1) glUniform1f(loc, v1);
    }
}

//--------------------------------------------------------------
void ofShader::setUniform2f(const string & name, float v1, float v2) const {
    if (bLoaded) {
        int loc = getUniformLocation(name);
        if (loc != -1) glUniform2f(loc, v1, v2);
    }
}

//--------------------------------------------------------------
void ofShader::setUniform3f(const string & name, float v1, float v2, float v3) const {
    if (bLoaded) {
        int loc = getUniformLocation(name);
        if (loc != -1) glUniform3f(loc, v1, v2, v3);
    }
}

//--------------------------------------------------------------
void ofShader::setUniform4f(const string & name, float v1, float v2, float v3, float v4) const {
    if (bLoaded) {
        int loc = getUniformLocation(name);
        if (loc != -1) glUniform4f(loc, v1, v2, v3, v4);
    }
}

//--------------------------------------------------------------
void ofShader::setUniform2f(const string & name, const glm::vec2 & v) const {
    setUniform2f(name, v.x, v.y);
}

//--------------------------------------------------------------
void ofShader::setUniform3f(const string & name, const glm::vec3 & v) const {
    setUniform3f(name, v.x, v.y, v.z);
}

//--------------------------------------------------------------
void ofShader::setUniform4f(const string & name, const glm::vec4 & v) const {
    setUniform4f(name, v.x, v.y, v.z, v.w);
}

//--------------------------------------------------------------
void ofShader::setUniform3f(const string & name, const ofFloatColor & v) const {
	setUniform3f(name, v.r, v.g, v.b);
}

//--------------------------------------------------------------
void ofShader::setUniform4f(const string & name, const ofFloatColor & v) const {
    setUniform4f(name, v.r, v.g, v.b, v.a);
}

//--------------------------------------------------------------
void ofShader::setUniform1iv(const string & name, const int * v, int count) const {
    if (bLoaded) {
        int loc = getUniformLocation(name);
        if (loc != -1) glUniform1iv(loc, count, v);
    }
}

//--------------------------------------------------------------
void ofShader::setUniform2iv(const string & name, const int * v, int count) const {
    if (bLoaded) {
        int loc = getUniformLocation(name);
        if (loc != -1) glUniform2iv(loc, count, v);
    }
}

//--------------------------------------------------------------
void ofShader::setUniform3iv(const string & name, const int * v, int count) const {
    if (bLoaded) {
        int loc = getUniformLocation(name);
        if (loc != -1) glUniform3iv(loc, count, v);
    }
}

//--------------------------------------------------------------
void ofShader::setUniform4iv(const string & name, const int * v, int count) const {
    if (bLoaded) {
        int loc = getUniformLocation(name);
        if (loc != -1) glUniform4iv(loc, count, v);
    }
}

//--------------------------------------------------------------
void ofShader::setUniform1fv(const string & name, const float * v, int count) const {
    if (bLoaded) {
        int loc = getUniformLocation(name);
        if (loc != -1) glUniform1fv(loc, count, v);
    }
}

//--------------------------------------------------------------
void ofShader::setUniform2fv(const string & name, const float * v, int count) const {
    if (bLoaded) {
        int loc = getUniformLocation(name);
        if (loc != -1) glUniform2fv(loc, count, v);
    }
}

//--------------------------------------------------------------
void ofShader::setUniform3fv(const string & name, const float * v, int count) const {
    if (bLoaded) {
        int loc = getUniformLocation(name);
        if (loc != -1) glUniform3fv(loc, count, v);
    }
}

//--------------------------------------------------------------
void ofShader::setUniform4fv(const string & name, const float * v, int count) const {
    if (bLoaded) {
        int loc = getUniformLocation(name);
        if (loc != -1) glUniform4fv(loc, count, v);
    }
}

//--------------------------------------------------------------
void ofShader::setUniforms(const ofParameterGroup & parameters) const {
    for (std::size_t i = 0; i < parameters.size(); i++) {
        if (parameters[i].type() == typeid(ofParameter<int>).name()) {
            setUniform1i(parameters[i].getEscapedName(), parameters[i].cast<int>());
        } else if (parameters[i].type() == typeid(ofParameter<float>).name()) {
            setUniform1f(parameters[i].getEscapedName(), parameters[i].cast<float>());
        } else if (parameters[i].type() == typeid(ofParameter<glm::vec2>).name()) {
            setUniform2f(parameters[i].getEscapedName(), parameters[i].cast<glm::vec2>());
        } else if (parameters[i].type() == typeid(ofParameter<glm::vec3>).name()) {
            setUniform3f(parameters[i].getEscapedName(), parameters[i].cast<glm::vec3>());
        } else if (parameters[i].type() == typeid(ofParameter<glm::vec4>).name()) {
            setUniform4f(parameters[i].getEscapedName(), parameters[i].cast<glm::vec4>());
        } else if (parameters[i].type() == typeid(ofParameter<ofVec2f>).name()) {
            setUniform2f(parameters[i].getEscapedName(), parameters[i].cast<glm::vec2>());
        } else if (parameters[i].type() == typeid(ofParameter<ofVec3f>).name()) {
            setUniform3f(parameters[i].getEscapedName(), parameters[i].cast<glm::vec3>());
        } else if (parameters[i].type() == typeid(ofParameter<ofVec4f>).name()) {
            setUniform4f(parameters[i].getEscapedName(), parameters[i].cast<glm::vec4>());
        } else if (parameters[i].type() == typeid(ofParameterGroup).name()) {
            setUniforms((ofParameterGroup &)parameters[i]);
        }
    }
}

//--------------------------------------------------------------
void ofShader::setUniformMatrix3f(const string & name, const glm::mat3 & m, int count) const {
    if (bLoaded) {
        int loc = getUniformLocation(name);
        if (loc != -1) glUniformMatrix3fv(loc, count, GL_FALSE, glm::value_ptr(m));
    }
}

//--------------------------------------------------------------
void ofShader::setUniformMatrix4f(const string & name, const glm::mat4 & m, int count) const {
    if (bLoaded) {
        int loc = getUniformLocation(name);
        if (loc != -1) glUniformMatrix4fv(loc, count, GL_FALSE, glm::value_ptr(m));
    }
}

#ifndef TARGET_OPENGLES
//--------------------------------------------------------------
void ofShader::setAttribute1s(GLint location, short v1) const {
    if (bLoaded)
        glVertexAttrib1s(location, v1);
}

//--------------------------------------------------------------
void ofShader::setAttribute2s(GLint location, short v1, short v2) const {
    if (bLoaded)
        glVertexAttrib2s(location, v1, v2);
}

//--------------------------------------------------------------
void ofShader::setAttribute3s(GLint location, short v1, short v2, short v3) const {
    if (bLoaded)
        glVertexAttrib3s(location, v1, v2, v3);
}

//--------------------------------------------------------------
void ofShader::setAttribute4s(GLint location, short v1, short v2, short v3, short v4) const {
    if (bLoaded)
        glVertexAttrib4s(location, v1, v2, v3, v4);
}
#endif

//--------------------------------------------------------------
void ofShader::setAttribute1f(GLint location, float v1) const {
    if (bLoaded)
        glVertexAttrib1f(location, v1);
}

//--------------------------------------------------------------
void ofShader::setAttribute2f(GLint location, float v1, float v2) const {
    if (bLoaded)
        glVertexAttrib2f(location, v1, v2);
}

//--------------------------------------------------------------
void ofShader::setAttribute3f(GLint location, float v1, float v2, float v3) const {
    if (bLoaded)
        glVertexAttrib3f(location, v1, v2, v3);
}

//--------------------------------------------------------------
void ofShader::setAttribute4f(GLint location, float v1, float v2, float v3, float v4) const {
    if (bLoaded)
        glVertexAttrib4f(location, v1, v2, v3, v4);
}

//--------------------------------------------------------------
void ofShader::setAttribute1fv(const string & name, const float * v, GLsizei stride) const {
    if (bLoaded) {
        GLint location = getAttributeLocation(name);
        if (location != -1) {
            glVertexAttribPointer(location, 1, GL_FLOAT, GL_FALSE, stride, v);
            glEnableVertexAttribArray(location);
        }
    }
}

//--------------------------------------------------------------
void ofShader::setAttribute2fv(const string & name, const float * v, GLsizei stride) const {
    if (bLoaded) {
        GLint location = getAttributeLocation(name);
        if (location != -1) {
            glVertexAttribPointer(location, 2, GL_FLOAT, GL_FALSE, stride, v);
            glEnableVertexAttribArray(location);
        }
    }
}

//--------------------------------------------------------------
void ofShader::setAttribute3fv(const string & name, const float * v, GLsizei stride) const {
    if (bLoaded) {
        GLint location = getAttributeLocation(name);
        if (location != -1) {
            glVertexAttribPointer(location, 3, GL_FLOAT, GL_FALSE, stride, v);
            glEnableVertexAttribArray(location);
        }
    }
}

//--------------------------------------------------------------
void ofShader::setAttribute4fv(const string & name, const float * v, GLsizei stride) const {
    if (bLoaded) {
        GLint location = getAttributeLocation(name);
        if (location != -1) {
            glVertexAttribPointer(location, 4, GL_FLOAT, GL_FALSE, stride, v);
            glEnableVertexAttribArray(location);
        }
    }
}

#ifndef TARGET_OPENGLES
//--------------------------------------------------------------
void ofShader::setAttribute1d(GLint location, double v1) const {
    if (bLoaded)
        glVertexAttrib1d(location, v1);
}

//--------------------------------------------------------------
void ofShader::setAttribute2d(GLint location, double v1, double v2) const {
    if (bLoaded)
        glVertexAttrib2d(location, v1, v2);
}

//--------------------------------------------------------------
void ofShader::setAttribute3d(GLint location, double v1, double v2, double v3) const {
    if (bLoaded)
        glVertexAttrib3d(location, v1, v2, v3);
}

//--------------------------------------------------------------
void ofShader::setAttribute4d(GLint location, double v1, double v2, double v3, double v4) const {
    if (bLoaded)
        glVertexAttrib4d(location, v1, v2, v3, v4);
}
#endif

//--------------------------------------------------------------
GLint ofShader::getAttributeLocation(const string & name) const {
    return glGetAttribLocation(program, name.c_str());
}

//--------------------------------------------------------------
GLint ofShader::getUniformLocation(const string & name) const {
    if (!bLoaded) return -1;
    auto it = uniformsCache.find(name);
    if (it == uniformsCache.end()) {
        return -1;
    } else {
        return it->second;
    }
}

#ifndef TARGET_OPENGLES
    #ifdef GLEW_ARB_uniform_buffer_object
//--------------------------------------------------------------
GLint ofShader::getUniformBlockIndex(const string & name) const {
    if (!bLoaded) return -1;

    if (GLEW_ARB_uniform_buffer_object) {
        auto it = uniformBlocksCache.find(name);
        if (it == uniformBlocksCache.end()) {
            return -1;
        } else {
            return it->second;
        }
    } else {
        ofLogError("ofShader::getUniformBlockIndex") << "Sorry, it looks like you can't run 'ARB_uniform_buffer_object'";
        return -1;
    }
}

//--------------------------------------------------------------
GLint ofShader::getUniformBlockBinding(const string & name) const {
    if (!bLoaded) return -1;

    if (GLEW_ARB_uniform_buffer_object) {
        GLint index = getUniformBlockIndex(name);
        if (index == -1) return -1;

        GLint blockBinding;
        glGetActiveUniformBlockiv(program, index, GL_UNIFORM_BLOCK_BINDING, &blockBinding);
        return blockBinding;
    } else {
        ofLogError("ofShader::getUniformBlockBinding") << "Sorry, it looks like you can't run 'ARB_uniform_buffer_object'";
        return -1;
    }
}

//--------------------------------------------------------------
void ofShader::printActiveUniformBlocks() const {
    if (GLEW_ARB_uniform_buffer_object) {
        GLint numUniformBlocks = 0;
        glGetProgramiv(program, GL_ACTIVE_UNIFORM_BLOCKS, &numUniformBlocks);
        ofLogNotice("ofShader") << numUniformBlocks << " uniform blocks";

        GLint uniformBlockMaxLength = 0;
        glGetProgramiv(program, GL_ACTIVE_UNIFORM_MAX_LENGTH, &uniformBlockMaxLength);

        GLchar * uniformBlockName = new GLchar[uniformBlockMaxLength];
        stringstream line;
        for (GLint i = 0; i < numUniformBlocks; i++) {
            GLsizei length;
            GLint blockBinding;
            GLsizei blockDataSize;
            glGetActiveUniformBlockName(program, i, uniformBlockMaxLength, &length, uniformBlockName);
            glGetActiveUniformBlockiv(program, i, GL_UNIFORM_BLOCK_BINDING, &blockBinding);
            glGetActiveUniformBlockiv(program, i, GL_UNIFORM_BLOCK_DATA_SIZE, &blockDataSize);

            line << " [" << i << "] ";
            for (int j = 0; j < length; j++) {
                line << uniformBlockName[j];
            }
            line << " @ index " << getUniformBlockIndex(uniformBlockName) << ", binding point " << blockBinding << ", size " << blockDataSize << " bytes";
            ofLogNotice("ofShader") << line.str();
            line.str("");
        }
        delete[] uniformBlockName;
    } else {
        ofLogError("ofShader::printActiveUniformBlocks") << "Sorry, it looks like you can't run 'ARB_uniform_buffer_object'";
    }
}

void ofShader::bindUniformBlock(GLuint binding, const string & name) const {
    if (bLoaded) {
        if (GLEW_ARB_uniform_buffer_object) {
            GLint index = getUniformBlockIndex(name);
            if (index != -1) {
                glUniformBlockBinding(program, index, binding);
            }
        } else {
            ofLogError("ofShader::bindUniformBlock") << "Sorry, it looks like you can't run 'ARB_uniform_buffer_object'";
        }
    }
}
    #endif
#endif

//--------------------------------------------------------------
void ofShader::printActiveUniforms() const {
    GLint numUniforms = 0;
    glGetProgramiv(program, GL_ACTIVE_UNIFORMS, &numUniforms);
    ofLogNotice("ofShader") << numUniforms << " uniforms";

    GLint uniformMaxLength = 0;
    glGetProgramiv(program, GL_ACTIVE_UNIFORM_MAX_LENGTH, &uniformMaxLength);

    GLint count = -1;
    GLenum type = 0;
    GLchar * uniformName = new GLchar[uniformMaxLength];
    for (GLint i = 0; i < numUniforms; i++) {
        stringstream line;
        GLsizei length;
        GLint location;
        glGetActiveUniform(program, i, uniformMaxLength, &length, &count, &type, uniformName);
        location = glGetUniformLocation(program, uniformName);
        if (location == -1) continue; // ignore uniform blocks

        line << "[" << location << "] ";
        for (int j = 0; j < length; j++) {
            line << uniformName[j];
        }
        line << " @ index " << location;
        ofLogNotice("ofShader") << line.str();
    }
    delete[] uniformName;
}

//--------------------------------------------------------------
void ofShader::printActiveAttributes() const {
    GLint numAttributes = 0;
    glGetProgramiv(program, GL_ACTIVE_ATTRIBUTES, &numAttributes);
    ofLogNotice("ofShader") << numAttributes << " attributes";

    GLint attributeMaxLength = 0;
    glGetProgramiv(program, GL_ACTIVE_ATTRIBUTE_MAX_LENGTH, &attributeMaxLength);

    GLint count = -1;
    GLenum type = 0;
    GLchar * attributeName = new GLchar[attributeMaxLength];
    stringstream line;
    for (GLint i = 0; i < numAttributes; i++) {
        GLsizei length;
        glGetActiveAttrib(program, i, attributeMaxLength, &length, &count, &type, attributeName);
        line << " [" << i << "] ";
        for (int j = 0; j < length; j++) {
            line << attributeName[j];
        }
        line << " @ index " << getAttributeLocation(attributeName);
        ofLogNotice("ofShader") << line.str();
        line.str("");
    }
    delete[] attributeName;
}

//----------------------------------------
bool ofShader::setShadowUniforms(int textureLocation) const {
    if (!ofIsGLProgrammableRenderer()) {
        return false;
    }

    setUniformTexture("uShadowCubeMap", ofShadow::getTextureTarget(OF_LIGHT_POINT), ofShadow::getPointTexId(), textureLocation);
    setUniformTexture("uShadowMapDirectional", ofShadow::getTextureTarget(OF_LIGHT_DIRECTIONAL), ofShadow::getDirectionalTexId(), textureLocation + 1);
    setUniformTexture("uShadowMapSpot", ofShadow::getTextureTarget(OF_LIGHT_SPOT), ofShadow::getSpotTexId(), textureLocation + 2);
    setUniformTexture("uShadowMapArea", ofShadow::getTextureTarget(OF_LIGHT_AREA), ofShadow::getAreaTexId(), textureLocation + 3);

    for (size_t i = 0; i < ofShadow::getShadowsData().size(); i++) {
        std::string idx = ofToString(i, 0);
        auto shadow = ofShadow::getShadowsData()[i].lock();
        std::string shadowAddress = "shadows[" + idx + "]";
        if (!shadow || !shadow->isEnabled || shadow->index < 0) {
            setUniform1f(shadowAddress + ".enabled", 0);
            continue;
        }
        setUniform1f(shadowAddress + ".enabled", 1);
        if (shadow->lightType != OF_LIGHT_POINT) {
            setUniformMatrix4f(shadowAddress + ".shadowMatrix", shadow->shadowMatrix);
        }

        setUniform1f(shadowAddress + ".near", shadow->nearClip);
        setUniform1f(shadowAddress + ".far", shadow->farClip);
        setUniform1f(shadowAddress + ".normalBias", shadow->normalBias);
        setUniform1f(shadowAddress + ".bias", shadow->bias);
        if (shadow->lightType != OF_LIGHT_POINT) {
            setUniform1f(shadowAddress + ".sampleRadius", shadow->sampleRadius / (float)ofShadow::getDepthMapWidth(shadow->lightType));
        } else {
            setUniform1f(shadowAddress + ".sampleRadius", shadow->sampleRadius);
        }

        setUniform3f(shadowAddress + ".lightWorldPos", shadow->position);
        setUniform1f(shadowAddress + ".strength", shadow->strength);
        setUniform3f(shadowAddress + ".lightUp", shadow->up);
        setUniform3f(shadowAddress + ".lightRight", shadow->right);
        setUniform1f(shadowAddress + ".shadowType", (float)shadow->shadowType);
        setUniform1i(shadowAddress + ".texIndex", shadow->texIndex);
    }
    return true;
}

//----------------------------------------
bool ofShader::setPbrEnvironmentMapUniforms(int textureLocation) const {
    if (!ofIsGLProgrammableRenderer()) {
        return false;
    }

    auto cubeMapData = ofCubeMap::getActiveData();
    if (cubeMapData) {
        if (cubeMapData->bIrradianceAllocated) {
            setUniformTexture("tex_irradianceMap", GL_TEXTURE_CUBE_MAP, cubeMapData->irradianceMapId, textureLocation);
        }
        if (cubeMapData->bPreFilteredMapAllocated) {
            setUniformTexture("tex_prefilterEnvMap", GL_TEXTURE_CUBE_MAP, cubeMapData->preFilteredMapId, textureLocation + 1);
        }
        if (cubeMapData->settings.useLutTex && ofCubeMap::getBrdfLutTexture().isAllocated()) {
            setUniformTexture("tex_brdfLUT", ofCubeMap::getBrdfLutTexture(), textureLocation + 2);
        }
    }
    return true;
}

//--------------------------------------------------------------
GLuint ofShader::getProgram() const {
    return program;
}

//--------------------------------------------------------------
GLuint ofShader::getShader(GLenum type) const {
    auto shader = shaders.find(type);
    if (shader != shaders.end()) {
        return shader->second.id;
    } else {
        return 0;
    }
}

//--------------------------------------------------------------
bool ofShader::operator==(const ofShader & other) const {
    return other.program == program;
}

//--------------------------------------------------------------
bool ofShader::operator!=(const ofShader & other) const {
    return other.program != program;
}

//--------------------------------------------------------------
string ofShader::nameForType(GLenum type) {
    switch (type) {
    case GL_VERTEX_SHADER:
        return "GL_VERTEX_SHADER";
    case GL_FRAGMENT_SHADER:
        return "GL_FRAGMENT_SHADER";
#ifndef TARGET_OPENGLES
    case GL_GEOMETRY_SHADER_EXT:
        return "GL_GEOMETRY_SHADER_EXT";
    #ifdef glDispatchCompute
    case GL_COMPUTE_SHADER:
        return "GL_COMPUTE_SHADER";
    #endif
#endif
    default:
        return "UNKNOWN SHADER TYPE";
    }
}
